今天要介紹的是 Observer 模式,這是 Gof 提出的模式之一,屬於行為型設計模式,這也是目前為止我覺得可以延伸最多應用案例的模式~
在一個應用程式中,有一個物件持有某些狀態,而其他物件對這些狀態的變化感興趣,並且需要根據這些變化進行對應的處理。當物件的狀態發生變化時,需要一個方法讓所有感興趣的物件即時得知,以便它們能夠相應地更新自己的行為。
在上述情境中,如何確保當物件狀態變化時,所有感興趣的物件都能夠即時更新,而不必手動逐一通知?如果每次狀態變化時都需要手動更新所有感興趣的物件,那程式碼將變得複雜且難以維護。尤其當需要增加或移除感興趣的物件時,會使程式碼耦合度過高。此外,如果某些物件未能及時更新,可能會導致應用程式的不一致性。
Observer 是這個情境與問題下的解決方式,GoF 將 Observer 模式定義為:「一個或多個觀察者對一個主體的狀態感興趣,並透過附加於主體來記錄他們對主體的興趣。當觀察者可能感興趣的主體發生變化時,會發送通知訊息去呼叫每個觀察者的更新方法。當觀察者不再對主體的狀態感興趣時,可以簡單地解除觀察。」
Observer 模式通常會有兩元素,一是主體(subject)物件,主體物件會維護依賴於它的客體(object)列表;二是客體(object),也就是對主體感興趣的物件,稱為觀察者(observer)。
整體運作方式就是當主體狀態有更改時,會以廣播的方式通知觀察者,通知內容可包含主體相關資料,而若觀察者不想收到通知,可將其從主體維護的觀察者列表移除。整體架構圖如下:
圖 1 Observer 模式架構圖(資料來源:自行繪製)
依照以上的架構圖,我們來實作一個 Observer 模式吧! 我們需要這四個元件:
接著就來實作吧~
ObserverList
class我們先建立 ObserverList
class,這是一個維護觀察者列表的 class,主體可利用此 class 來建立主體可能擁有的依賴觀察者列表。
class ObserverList {
constructor() {
this.observerList = [];
}
add(obj) {
return this.observerList.push(obj);
}
count() {
return this.observerList.length;
}
get(index) {
if (index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
}
indexOf(obj, startIndex) {
let i = startIndex;
while (i < this.observerList.length) {
if (this.observerList[i] === obj) {
return i;
}
i++;
}
return -1;
}
removeAt(index) {
this.observerList.splice(index, 1);
}
}
在這個觀察者列表中會用陣列來儲存對主體感興趣的所有物件,並且有新增和移除觀察者的方法,我們可依據需要去調整這個列表的資料。
Subject
class接著建立主體的 class,在主體 class 中我們會建立一個觀察者列表的實例,並透過主體的方法來呼叫觀察者列表的增加和移除,以及定義主體的通知(notify)方法,在需要時呼叫觀察者列表中每個觀察者的更新方法。
class Subject {
constructor() {
this.observers = new ObserverList();
}
addObserver(observer) {
this.observers.add(observer);
}
removeObserver(observer) {
this.observers.removeAt(
this.observers.indexOf(observer, 0)
);
}
notify(context) {
// 依序呼叫觀察者列表中的每個觀察者(observer)的 update 方法
const observerCount = this.observers.count();
for (let i = 0; i < observerCount; i++) {
this.observers.get(i).update(context);
}
}
}
Observer
classObserver
class 是用來建立觀察者實例,觀察者最重要的方法就是 update
方法,這樣當主體狀態變化要通知觀察者時,才能呼叫觀察者更新方法,這裡我們先寫一個大概架構,等等會在 ConcreteObserver
補足它。
class Observer {
constructor() {}
update() {
// ...
}
}
鋪陳這麼多抽象類別,就要來看看我們的應用情境,到底誰是我們要觀察的主體,有哪些觀察者、觀察者該如何更新?
我們以勾選框的情境作為範例,當應用程式中有多個確認事項需要使用者逐一勾選時,介面上會出現大量的勾選框。這時,如果有一個「全部勾選」的選項,使用者只需點選一次,就能勾選所有項目,而不需要逐一點選每個勾選框。在這情況中,主體是「全部勾選」的勾選框,其他個別的勾選框則是觀察者,當「全部勾選」的狀態改變時,它會通知所有觀察者(即其他勾選框)更新自己的勾選狀態。
示意圖如下,觀察者先訂閱主體:
圖 2 觀察者先訂閱主體示意圖(資料來源:自行繪製)
接著,當主體「全部勾選」勾選框狀態改變時,就會通知觀察者,並執行觀察者的更新方法,也就是更新各自的勾選狀態:
圖 3 主體通知觀察者示意圖(資料來源:自行繪製)
因此,我們的 HTML 需要包含一個主體勾選框,以及按鈕來新增或移除觀察者勾選框。
<button id="addNewObserver">Add New Observer Checkbox</button>
<button id="removeObservers">Remove Observers</button>
<div class='main-check-block'>
<label>
<input id="mainCheckbox" type="checkbox" />
全部勾選
</label>
</diV>
<div id="observersContainer"></div>
ConcreteSubject
和 ConcreteObserver接著我們要定義 ConcreteSubject
和 ConcreteObserver
,以增加新觀察者並實作更新介面,ConcreteSubject
class 內就是主體勾選框,被點擊時會呼叫 notify
,而這個 notify
就是前面定義的 Subject
class 的通知方法。ConcreteObserver
class 內則是每一個觀察者的勾選框,它的 update
方法就是更新勾選框的 check
值。
// ConcreteSubject class
class ConcreteSubject extends Subject {
constructor(element) {
super();
this.element = element;
this.element.onclick = () => {
this.notify(this.element.checked);
};
}
}
// ConcreteObserver class
class ConcreteObserver extends Observer {
constructor(element) {
super();
this.element = element;
}
update(value) {
this.element.checked = value;
}
}
最後將我們準備的這些材料們集合起來~將方法綁定到 DOM 元素,完成整個流程:
const addBtn = document.getElementById('addNewObserver');
const container = document.getElementById('observersContainer');
const removeBtn = document.getElementById('removeObservers');
const mainCheckbox = document.getElementById('mainCheckbox');
const controlCheckbox = new ConcreteSubject(mainCheckbox); // 建立主體勾選框
const addNewObserver = () => {
const observerWrapper = document.createElement('div');
observerWrapper.classList.add('observer');
const check = document.createElement('input');
check.type = 'checkbox';
const label = document.createElement('label');
label.textContent = `checkbox item`;
label.prepend(check);
observerWrapper.appendChild(label);
const checkObserver = new ConcreteObserver(check); // 每新增一個勾選框就建立一個 Observer 觀察者
controlCheckbox.addObserver(checkObserver); // 將觀察者加入主體的觀察者列表中
container.appendChild(observerWrapper);
};
const removeObservers = () => {
const checkboxes = container.getElementsByClassName('observer');
while (checkboxes.length > 0) {
const checkbox = checkboxes[0];
container.removeChild(checkbox);
}
};
addBtn.onclick = addNewObserver;
removeBtn.onclick = removeObservers;
最後完成的樣子截圖如下:
圖 4 Checkbox Demo 截圖(資料來源:自行截圖)
完整的程式碼請見連結:Observer Pattern Checkbox Demo
因為勾選框的範例十分簡單明瞭,可能有人會覺得前面準備好多 class,也太大費周章了吧~以完整性來說,是可以分為四個 class(主體、觀察者、具體主體、具體觀察者)沒錯,但其實簡易版我們可以只分成兩個元素就好,就是主體與觀察者。
主體稱為 subject,但也有人稱它為 observable,也就是被觀察的對象;而觀察者就是 observers。所以經常會看到 observable、observers 這兩個詞彙,兩者是代表不同的元素哦! 接下來看看如果是簡單版的 observer 模式該如何實作,簡單版我就以 observable、observers 這兩個詞彙來稱呼主體與觀察者囉~
首先一樣需要由 observable 來儲存觀察者列表,並且要能新增和移除觀察者,要能通知觀察者,其實就是把前面我們提的 ObserverList
的方法一起寫進 observable 了。因此 observable 通常會具有下列功能和元素:
observers
:一個儲存 observer 的陣列,代表對這主體有興趣的 observer 們,這些 observer 可能是函式也可能是物件,若是物件則該物件內會有可被執行的更新函式(update function),這樣主體狀態變化時才能呼叫這些 observer 項目的函式subscribe()
:把 observer 加入 observers 陣列中的方法unsubscribe()
:把 observer 從 observers 陣列中移除的方法notify()
:一個用來通知此 observable 中所有 observers 的方法,當此方法被呼叫時,observers 陣列內的所有 observer 都會被執行。也有人稱作 dispatch()
或 broadcast()
依照上述元素可建立 Observable class 如下:
class Observable {
constructor() {
this.observers = [];
}
subscribe(func) {
this.observers.push(func);
// 回傳一個取消訂閱的函式
return () => {
const index = this.observers.indexOf(func);
this.observers.splice(index, 1);
};
}
unsubscribe(func) {
this.observers = this.observers.filter((observer) => observer !== func);
}
notify(data) {
this.observers.forEach((observer) => observer(data));
}
}
小提醒,在前面的勾選框範例中,觀察者(observer)是一個包含 update
函式的物件。而在我們現在的 Observable
class 中,觀察者 observer 預設為直接的函式。在需要通知觀察者時,會直接執行這些函式。
接著來看如何應用,模擬一個簡單的購物車應用,當商品被加到購物車時,觀察者會收到通知並更新購物車的總金額和物品清單。
// 定義購物車更新函式,也就是 observer
function updateCartTotal(data) {
console.log(`Cart Total: $${data.total}`);
};
function updateCartItems(data) {
console.log(`Items in Cart: ${data.items.join(', ')}`);
};
// 建立一個購物車 subject/observable
const cartObservable = new Observable();
// 訂閱主體,同時儲存取消訂閱的函式
const unsubscribeTotal = cartObservable.subscribe(updateCartTotal);
const unsubscribeItems = cartObservable.subscribe(updateCartItems);
// 增加商品到購物車並通知觀察者
cartObservable.notify({ total: 50, items: ['Apple', 'Banana'] });
// 接著取消購物車總金額的訂閱
console.log('--- unsubscribe ---');
unsubscribeTotal();
// 此時再新增另一個商品到購物車,這次只會更新商品清單
cartObservable.notify({ total: 35, items: ['Orange'] });
// Cart Total: $50
// Items in Cart: Apple, Banana
// --- unsubscribe ---
// Items in Cart: Orange
當我們需要綁定事件到 DOM 元素上時,這過程就類似 Observer 模式:
const myButton = document.querySelector('#myButton');
function handleClick(event) {
console.log('myButton was clicked!');
}
myButton.addEventListener('click', handleClick);
在這裡,myButton
是主體(subject/observable),而 handleClick
(即點擊後要執行的 callback function)就是觀察者(observer)。addEventListener
就像是 subscribe()
,它負責將觀察者新增到 myButton
主體中。
當按鈕的點擊事件發生變化時,myButton
會通知所有的觀察者(即事件監聽器),並執行這些 callback function。因此,我們可以對 myButton
多次使用 addEventListener
來增加不同的 callback function。myButton
會維護一個觀察者的列表(所有的事件監聽器),並在點擊時透過迴圈逐一執行這些 callback function,確保每個綁定的 callback 都能被觸發。
這種設計讓我們能將多個行為綁定到同一個事件上,並確保每個行為在事件發生時都能被執行。
Redux 的 Store 基本上也運用了 Observer 模式,用來通知使用該資料的元件 re-render。以下是官方文件中的一個微型範例,展示了 Redux store 的內部運作原理:
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
dispatch({ type: '@@redux/INIT' })
return { dispatch, subscribe, getState }
}
其中,listeners
陣列相當於觀察者列表,負責儲存對 store 感興趣的觀察者。subscribe
函式則用來將觀察者加到這個陣列中,並回傳一個 unsubscribe
函式,以便將觀察者從列表中移除。而 dispatch
函式相當於 notify
方法,它會遍歷 listeners
陣列並逐一呼叫這些觀察者。
因此,Redux 的 createStore
所建立的 store 其實就是一個主體(subject/observable)。當 store 的 dispatch
方法被呼叫時,它會通知所有的觀察者(listeners),讓他們知道該更新狀態了。
更多可參考官方文件 Inside a Redux Store。
RxJS 是個使用 Observer 模式的熱門程式庫,RxJS 的官方文件這樣介紹自己:「ReactiveX 將 Observer 模式與迭代器模式以及函數式程式設計與集合相結合,以滿足對事件管理序列的理想方式需求。(ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events)」
因為 RxJS 又是一門很深的學問了,這裡就先不深入太多,希望未來有機會介紹它。(我也是近期工作上才接觸到 RxJS,仍在努力學習中!)
對 RxJS 有興趣的我大推 Huli 大大的 希望是最淺顯易懂的 RxJS 教學 這篇文章,真的是讓對 RxJS 完全陌生的我學到很多!
剛好看到 react query 的相關文章,有提及 useQuery
底層邏輯也用到了 observer 的概念去實作,對 react query 運作邏輯有興趣的可以參考:[npm] react-query、深入淺出 TanStack Query(一):在呼叫 useQuery 後發生了什麼事
以 Observer 作為解決方案優點如下:
以 Observer 作為解決方案優點如下:
感覺光是 Observer 模式和延伸的應用案例就可以寫好幾篇了呀...,除了文章提到的案例,其實還有很多情況都可使用 Observer 模式的解法,也有許多我還沒提到的案例,如果有讀者想到也很歡迎提出討論!
在《JavaScript 設計模式學習手冊 第二版》書中有提到,Observer 模式有助於應用程式中的解耦,它是最容易上手的設計模式之一、也是最強大的設計模式之一,很鼓勵不熟悉 Observer 的讀者試著了解或是嘗試實作看看~
接著還有一個與 Observer 很類似的 Publish/Subscribe 模式,我會在下一篇繼續介紹~